官方文件提到 Worklet 是輕量級的 web worker,使網路開發人員可以存取低階的渲染管道
MDN 上列出來的有三種,但其中的 AnimationWorklet 跟 LayoutWorklet 似乎都還在實驗階段,有興趣瞭解的人可以參考連結所附的介紹文章
而今天打算只介紹控制音訊的 AudioWorklet
在 AudioWorklet 出現之前,瀏覽器提供了 ScriptProcessorNode 供開發者在網頁上處理音訊,但使用 ScriptProcessorNode 操作音訊是執行在主線程上面的,因此有可能導致 UI 畫面的阻塞,所以在 Chrome 64 後才推出了 AudioWorklet,AudioWorklet 運行在額外的音訊線程(AudioWorkletGlobalScope),避免了主線程資源的佔用
以下我們直接透過 Google AudioWorklet example,實際看看怎麼在網頁上操作音訊
範例中在按下按鈕後網頁會播放一聲 440Hz 的聲音,請大家在操作前先把聲音調小,不然可能會突然嚇到

建立 AudioContext
首先所有音訊操作的都需要在一個 音訊上下文中(AudioContext),所以一開始會先建立 AudioContext,接著再按下 START 按鈕後執行 startAudio
const audioContext = new AudioContext();
window.addEventListener('load', async () => {
  const buttonEl = document.getElementById('button-start');
  buttonEl.disabled = false;
  buttonEl.addEventListener('click', async () => {
    await startAudio(audioContext);
    audioContext.resume();
    buttonEl.disabled = true;
    buttonEl.textContent = 'Playing...';
  }, false);
});
開始播放音訊
const startAudio = async (context) => {
  await context.audioWorklet.addModule('bypass-processor.js');
  const oscillator = new OscillatorNode(context);
  const bypasser = new AudioWorkletNode(context, 'bypass-processor');
  oscillator.connect(bypasser).connect(context.destination);
  oscillator.start();
};
首先會呼叫 Worklet.addModule 將自定義的 bypass-processor.js (後面會再介紹到這個檔案) 以模組方式加載到 AudioContext 裡
await context.audioWorklet.addModule('bypass-processor.js');
接著呼叫 OscillatorNode,OscillatorNode 可以建構新的音訊波形,預設波形是正弦波(sine)、頻率是 440 Hz
// context - 使用的 AudioContext 
// options - 可以設定波形、頻率等參數
const oscillator = new OscillatorNode(context, options);
例如:以下建立 660 Hz 的正弦波
const options = {
  type: 'sine',
  frequency: 660
}
const oscillator = new OscillatorNode(context, options);
下一步則是建立 AudioWorkletNode 實例,AudioWorkletNode 代表一個自定義的音訊節點,第二個參數就是我們之前加載進來的檔案名稱
const bypasser = new AudioWorkletNode(context, 'bypass-processor');
接著會將建立好的 OscillatorNode 連接到自定義的 bypasser(AudioWorkletNode),再連接到整個 AudioContext 的終點 (destination),這個終點可以視為播放聲音出來的設備,例如喇叭
oscillator.connect(bypasser).connect(context.destination);
最後呼叫 start 方法,開始播放音訊
oscillator.start();
上面這一連串使用到的方法,乍看之下似乎難以理解,但搭配以下這張 音訊路由圖 大概會有點感覺,整個音訊的操作都需要在黃色範圍內的 AudioContext 裡處理,而以上程式碼所做的事情大部分就是創立 音訊節點(AudioNode),並把各節點以 connect 的方式連接起來,當所有音訊節點的關聯性都連結好後,最後一步是呼叫 connect(context.destination),將所有節點連接到右下角的擴音設備,準備進行播放
在創建自訂音訊的時候有以下幾個步驟:
bypass-processor.js
AudioContext 中其中 1~3 步驟如以下所示,而 4~6 步驟是上面的 startAudio 函式中提過的
以下 bypass-processor.js 檔案負責處理自訂音訊,input 跟 output 分別對應輸入跟輸出的音源,其中的 BypassProcessor 單純複製輸入的音源到輸出,沒有做任何額外處理,process 方法的 回傳值 為 true 強制使 AudioWorkletNode 的狀態是 active 的
最後一行呼叫 registerProcessor 註冊 BypassProcessor 並給他一個名稱 'bypass-processor'
// bypass-processor.js
class BypassProcessor extends AudioWorkletProcessor {
  process(inputs, outputs) {
    // 預設只有單一個 input 跟 output.
    const input = inputs[0];
    const output = outputs[0];
    // 單純複製輸入的音源到輸出,沒有做任何額外處理
    for (let channel = 0; channel < output.length; ++channel) {
      output[channel].set(input[channel]);
    }
    return true;
  }
}
registerProcessor('bypass-processor', BypassProcessor);
結合以上程式碼,範例最終會輸出一個 440 Hz 的正弦波。
以上範例沒有額外處理音訊的輸入、輸出,但要寫出程式碼還是蠻複雜的,實在沒想到在網頁中處理音訊這麼困難
總之最後藉由這個範例大致上瞭解了 AudioWorklet 的用法,以及知道它可以使用額外獨立的線程處理音訊,避免影響到主線程 UI 的渲染